/*******************************************************************************
 * Copyright (c) 2012-2016 Codenvy, S.A.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *   Codenvy, S.A. - initial API and implementation
 *******************************************************************************/
package org.eclipse.che.plugin.jdb.server;

import com.sun.jdi.AbsentInformationException;
import com.sun.jdi.Bootstrap;
import com.sun.jdi.ClassNotPreparedException;
import com.sun.jdi.IncompatibleThreadStateException;
import com.sun.jdi.NativeMethodException;
import com.sun.jdi.ReferenceType;
import com.sun.jdi.ThreadReference;
import com.sun.jdi.VMCannotBeModifiedException;
import com.sun.jdi.VirtualMachine;
import com.sun.jdi.connect.AttachingConnector;
import com.sun.jdi.connect.Connector;
import com.sun.jdi.connect.IllegalConnectorArgumentsException;
import com.sun.jdi.request.BreakpointRequest;
import com.sun.jdi.request.ClassPrepareRequest;
import com.sun.jdi.request.EventRequest;
import com.sun.jdi.request.EventRequestManager;
import com.sun.jdi.request.InvalidRequestStateException;
import com.sun.jdi.request.StepRequest;

import org.eclipse.che.api.debug.shared.dto.BreakpointDto;
import org.eclipse.che.api.debug.shared.dto.FieldDto;
import org.eclipse.che.api.debug.shared.dto.LocationDto;
import org.eclipse.che.api.debug.shared.dto.StackFrameDumpDto;
import org.eclipse.che.api.debug.shared.dto.VariableDto;
import org.eclipse.che.api.debug.shared.dto.VariablePathDto;
import org.eclipse.che.api.debug.shared.dto.action.ResumeActionDto;
import org.eclipse.che.api.debug.shared.model.Breakpoint;
import org.eclipse.che.api.debug.shared.model.DebuggerInfo;
import org.eclipse.che.api.debug.shared.model.Location;
import org.eclipse.che.api.debug.shared.model.SimpleValue;
import org.eclipse.che.api.debug.shared.model.Variable;
import org.eclipse.che.api.debug.shared.model.VariablePath;
import org.eclipse.che.api.debug.shared.model.action.ResumeAction;
import org.eclipse.che.api.debug.shared.model.action.StartAction;
import org.eclipse.che.api.debug.shared.model.action.StepIntoAction;
import org.eclipse.che.api.debug.shared.model.action.StepOutAction;
import org.eclipse.che.api.debug.shared.model.action.StepOverAction;
import org.eclipse.che.api.debug.shared.model.impl.DebuggerInfoImpl;
import org.eclipse.che.api.debug.shared.model.impl.FieldImpl;
import org.eclipse.che.api.debug.shared.model.impl.SimpleValueImpl;
import org.eclipse.che.api.debug.shared.model.impl.VariableImpl;
import org.eclipse.che.api.debug.shared.model.impl.event.BreakpointActivatedEventImpl;
import org.eclipse.che.api.debug.shared.model.impl.event.DisconnectEventImpl;
import org.eclipse.che.api.debug.shared.model.impl.event.SuspendEventImpl;
import org.eclipse.che.api.debugger.server.Debugger;
import org.eclipse.che.api.debugger.server.exceptions.DebuggerException;
import org.eclipse.che.plugin.jdb.server.exceptions.DebuggerAbsentInformationException;
import org.eclipse.che.plugin.jdb.server.expression.Evaluator;
import org.eclipse.che.plugin.jdb.server.expression.ExpressionException;
import org.eclipse.che.plugin.jdb.server.expression.ExpressionParser;
import org.eclipse.che.plugin.jdb.server.utils.JavaDebuggerUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;
import static org.eclipse.che.dto.server.DtoFactory.newDto;

/**
 * Connects to JVM over Java Debug Wire Protocol handle its events. All methods of this class may throws
 * DebuggerException. Typically such exception caused by errors in underlying JDI (Java Debug Interface), e.g.
 * connection errors. Instance of Debugger is not thread-safe.
 *
 * @author andrew00x
 * @author Artem Zatsarynnyi
 * @author Valeriy Svydenko
 */
public class JavaDebugger implements EventsHandler, Debugger {
    private static final Logger            LOG          = LoggerFactory.getLogger(JavaDebugger.class);
    private static final JavaDebuggerUtils debuggerUtil = new JavaDebuggerUtils();

    private final String           host;
    private final int              port;
    private final DebuggerCallback debuggerCallback;

    /**
     * A mapping of source file names to breakpoints. This mapping is used to set
     * breakpoints in files that haven't been loaded yet by a target Java VM.
     */
    private final ConcurrentMap<String, List<Breakpoint>> deferredBreakpoints = new ConcurrentHashMap<>();

    /** Stores ClassPrepareRequests to prevent making duplicate class prepare requests. */
    private final ConcurrentMap<String, ClassPrepareRequest> classPrepareRequests = new ConcurrentHashMap<>();

    /** Target Java VM representation. */
    private VirtualMachine  vm;
    private EventsCollector eventsCollector;

    /** Current thread. Not <code>null</code> is thread suspended, e.g breakpoint reached. */
    private ThreadReference thread;
    /** Current stack frame. Not <code>null</code> is thread suspended, e.g breakpoint reached. */
    private JdiStackFrame   stackFrame;
    /** Lock for synchronization debug processes. */
    private Lock lock = new ReentrantLock();

    /**
     * Create debugger and connect it to the JVM which already running at the specified host and port.
     *
     * @param host
     *         the host where JVM running
     * @param port
     *         the Java Debug Wire Protocol (JDWP) port
     * @throws DebuggerException
     *         when connection to Java VM is not established
     */
    JavaDebugger(String host, int port, DebuggerCallback debuggerCallback) throws DebuggerException {
        this.host = host;
        this.port = port;
        this.debuggerCallback = debuggerCallback;
        connect();
    }

    /**
     * Attach to a JVM that is already running at specified host.
     *
     * @throws DebuggerException
     *         when connection to Java VM is not established
     */
    private void connect() throws DebuggerException {
        final String connectorName = "com.sun.jdi.SocketAttach";
        AttachingConnector connector = connector(connectorName);
        if (connector == null) {
            throw new DebuggerException(
                    String.format("Unable connect to target Java VM. Requested connector '%s' not found. ", connectorName));
        }
        Map<String, Connector.Argument> arguments = connector.defaultArguments();
        arguments.get("hostname").setValue(host);
        ((Connector.IntegerArgument)arguments.get("port")).setValue(port);
        int attempt = 0;
        for (; ; ) {
            try {
                Thread.sleep(2000);
                vm = connector.attach(arguments);
                vm.suspend();
                break;
            } catch (UnknownHostException | IllegalConnectorArgumentsException e) {
                throw new DebuggerException(e.getMessage(), e);
            } catch (IOException e) {
                LOG.error(e.getMessage(), e);
                if (++attempt > 10) {
                    throw new DebuggerException(e.getMessage(), e);
                }
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException ignored) {
                }
            } catch (InterruptedException ignored) {
            }
        }
        eventsCollector = new EventsCollector(vm.eventQueue(), this);
        LOG.debug("Connect {}:{}", host, port);
    }

    private AttachingConnector connector(String connectorName) {
        for (AttachingConnector c : Bootstrap.virtualMachineManager().attachingConnectors()) {
            if (connectorName.equals(c.name())) {
                return c;
            }
        }
        return null;
    }

    @Override
    public DebuggerInfo getInfo() throws DebuggerException {
        return new DebuggerInfoImpl(host, port, vm.name(), vm.version(), 0, null);
    }

    @Override
    public void start(StartAction action) throws DebuggerException {
        for (Breakpoint b : action.getBreakpoints()) {
            try {
                addBreakpoint(b);
            } catch (DebuggerException e) {
                // can't add breakpoint, skip it
            }
        }
        vm.resume();
    }

    @Override
    public void disconnect() throws DebuggerException {
        resume(newDto(ResumeActionDto.class));
        vm.dispose();
        LOG.debug("Close connection to {}:{}", host, port);
    }

    @Override
    public void addBreakpoint(Breakpoint breakpoint) throws DebuggerException {
        final String className = findFQN(breakpoint);
        final int lineNumber = breakpoint.getLocation().getLineNumber();
        List<ReferenceType> classes = vm.classesByName(className);
        // it may mean that class doesn't loaded by a target JVM yet
        if (classes.isEmpty()) {
            deferBreakpoint(breakpoint);
            throw new DebuggerException("Class not loaded");
        }

        ReferenceType clazz = classes.get(0);
        List<com.sun.jdi.Location> locations;
        try {
            locations = clazz.locationsOfLine(lineNumber);
        } catch (AbsentInformationException | ClassNotPreparedException e) {
            throw new DebuggerException(e.getMessage(), e);
        }

        if (locations.isEmpty()) {
            throw new DebuggerException("Line " + lineNumber + " not found in class " + className);
        }

        com.sun.jdi.Location location = locations.get(0);
        if (location.method() == null) {
            // Line is out of method.
            throw new DebuggerException("Invalid line " + lineNumber + " in class " + className);
        }

        // Ignore new breakpoint if already have breakpoint at the same location.
        EventRequestManager requestManager = getEventManager();
        for (BreakpointRequest breakpointRequest : requestManager.breakpointRequests()) {
            if (location.equals(breakpointRequest.location())) {
                LOG.debug("Breakpoint at {} already set", location);
                return;
            }
        }

        try {
            EventRequest breakPointRequest = requestManager.createBreakpointRequest(location);
            breakPointRequest.setSuspendPolicy(EventRequest.SUSPEND_ALL);
            String expression = breakpoint.getCondition();
            if (!(expression == null || expression.isEmpty())) {
                ExpressionParser parser = ExpressionParser.newInstance(expression);
                breakPointRequest.putProperty("org.eclipse.che.ide.java.debug.condition.expression.parser", parser);
            }
            breakPointRequest.setEnabled(true);
        } catch (NativeMethodException | IllegalThreadStateException | InvalidRequestStateException e) {
            throw new DebuggerException(e.getMessage(), e);
        }

        debuggerCallback.onEvent(new BreakpointActivatedEventImpl(breakpoint));
        LOG.debug("Add breakpoint: {}", location);
    }

    private String findFQN(Breakpoint breakpoint) throws DebuggerException {
        Location location = breakpoint.getLocation();
        final String parentFqn = location.getTarget();
        final String projectPath = location.getResourceProjectPath();
        int lineNumber = location.getLineNumber();

        return debuggerUtil.findFqnByPosition(projectPath, parentFqn, lineNumber);
    }

    private void deferBreakpoint(Breakpoint breakpoint) throws DebuggerException {
        final String className = breakpoint.getLocation().getTarget();
        List<Breakpoint> newList = new ArrayList<>();
        List<Breakpoint> list = deferredBreakpoints.putIfAbsent(className, newList);
        if (list == null) {
            list = newList;
        }
        list.add(breakpoint);

        // start listening for the load of the type
        if (!classPrepareRequests.containsKey(className)) {
            ClassPrepareRequest request = getEventManager().createClassPrepareRequest();
            // set class filter in order to reduce the amount of event traffic sent from the target VM to the debugger VM
            request.addClassFilter(className);
            request.enable();
            classPrepareRequests.put(className, request);
        }

        LOG.debug("Deferred breakpoint: {}", breakpoint.getLocation());
    }

    @Override
    public List<Breakpoint> getAllBreakpoints() throws DebuggerException {
        List<BreakpointRequest> breakpointRequests;
        try {
            breakpointRequests = getEventManager().breakpointRequests();
        } catch (DebuggerException e) {
            Throwable cause = e.getCause();
            if (cause instanceof VMCannotBeModifiedException) {
                // If target VM in read-only state then list of break point always empty.
                return Collections.emptyList();
            }
            throw e;
        }
        List<Breakpoint> breakPoints = new ArrayList<>(breakpointRequests.size());
        for (BreakpointRequest breakpointRequest : breakpointRequests) {
            com.sun.jdi.Location location = breakpointRequest.location();
            // Breakpoint always enabled at the moment. Managing states of breakpoint is not supported for now.
            breakPoints.add(newDto(BreakpointDto.class).withEnabled(true)
                                                       .withLocation(newDto(LocationDto.class).withTarget(location.declaringType().name())
                                                                                              .withLineNumber(location.lineNumber())));
        }
        Collections.sort(breakPoints, BREAKPOINT_COMPARATOR);
        return breakPoints;
    }

    private static final Comparator<Breakpoint> BREAKPOINT_COMPARATOR = new BreakPointComparator();

    @Override
    public void deleteBreakpoint(Location location) throws DebuggerException {
        final String className = location.getTarget();
        final int lineNumber = location.getLineNumber();
        EventRequestManager requestManager = getEventManager();
        List<BreakpointRequest> snapshot = new ArrayList<>(requestManager.breakpointRequests());
        for (BreakpointRequest breakpointRequest : snapshot) {
            com.sun.jdi.Location jdiLocation = breakpointRequest.location();
            if (jdiLocation.declaringType().name().equals(className) && jdiLocation.lineNumber() == lineNumber) {
                requestManager.deleteEventRequest(breakpointRequest);
                LOG.debug("Delete breakpoint: {}", location);
            }
        }
    }

    @Override
    public void deleteAllBreakpoints() throws DebuggerException {
        getEventManager().deleteAllBreakpoints();
    }

    @Override
    public void resume(ResumeAction action) throws DebuggerException {
        try {
            vm.resume();
            LOG.debug("Resume VM");
        } catch (VMCannotBeModifiedException e) {
            throw new DebuggerException(e.getMessage(), e);
        } finally {
            resetCurrentThread();
        }
    }

    @Override
    public StackFrameDumpDto dumpStackFrame() throws DebuggerException {
        lock.lock();
        try {
            final JdiStackFrame currentFrame = getCurrentFrame();
            StackFrameDumpDto dump = newDto(StackFrameDumpDto.class);
            boolean existInformation = true;
            JdiLocalVariable[] variables = new JdiLocalVariable[0];
            try {
                variables = currentFrame.getLocalVariables();
            } catch (DebuggerAbsentInformationException e) {
                existInformation = false;
            }
            for (JdiField f : currentFrame.getFields()) {
                List<String> variablePath = asList(f.isStatic() ? "static" : "this", f.getName());
                dump.getFields().add(newDto(FieldDto.class).withIsFinal(f.isFinal())
                                                           .withIsStatic(f.isStatic())
                                                           .withIsTransient(f.isTransient())
                                                           .withIsVolatile(f.isVolatile())
                                                           .withName(f.getName())
                                                           .withExistInformation(existInformation)
                                                           .withValue(f.getValue().getAsString())
                                                           .withType(f.getTypeName())
                                                           .withVariablePath(newDto(VariablePathDto.class).withPath(variablePath))
                                                           .withPrimitive(f.isPrimitive()));
            }
            for (JdiLocalVariable var : variables) {
                dump.getVariables().add(newDto(VariableDto.class).withName(var.getName())
                                                                 .withExistInformation(existInformation)
                                                                 .withValue(var.getValue().getAsString())
                                                                 .withType(var.getTypeName())
                                                                 .withVariablePath(
                                                                         newDto(VariablePathDto.class)
                                                                                 .withPath(singletonList(var.getName()))
                                                                                  )
                                                                 .withPrimitive(var.isPrimitive()));
            }
            return dump;
        } finally {
            lock.unlock();
        }
    }

    /**
     * Get value of variable with specified path. Each item in path is name of variable.
     * <p>
     * Path must be specified according to the following rules:
     * <ol>
     * <li>If need to get field of this object of current frame then first element in array always should be
     * 'this'.</li>
     * <li>If need to get static field in current frame then first element in array always should be 'static'.</li>
     * <li>If need to get local variable in current frame then first element should be the name of local variable.</li>
     * </ol>
     * </p>
     * Here is example. <br/>
     * Assume we have next hierarchy of classes and breakpoint set in line: <i>// breakpoint</i>:
     * <pre>
     *    class A {
     *       private String str;
     *       ...
     *    }
     *
     *    class B {
     *       private A a;
     *       ....
     *
     *       void method() {
     *          A var = new A();
     *          var.setStr(...);
     *          a = var;
     *          // breakpoint
     *       }
     *    }
     * </pre>
     * There are two ways to access variable <i>str</i> in class <i>A</i>:
     * <ol>
     * <li>Through field <i>a</i> in class <i>B</i>: ['this', 'a', 'str']</li>
     * <li>Through local variable <i>var</i> in method <i>B.method()</i>: ['var', 'str']</li>
     * </ol>
     *
     * @param variablePath
     *         path to variable
     * @return variable or <code>null</code> if variable not found
     * @throws DebuggerException
     *         when any other errors occur when try to access the variable
     */
    @Override
    public SimpleValue getValue(VariablePath variablePath) throws DebuggerException {
        List<String> path = variablePath.getPath();
        if (path.size() == 0) {
            throw new IllegalArgumentException("Path to value may not be empty. ");
        }
        JdiVariable variable;
        int offset;
        if ("this".equals(path.get(0)) || "static".equals(path.get(0))) {
            if (path.size() < 2) {
                throw new IllegalArgumentException("Name of field required. ");
            }
            variable = getCurrentFrame().getFieldByName(path.get(1));
            offset = 2;
        } else {
            try {
                variable = getCurrentFrame().getLocalVariableByName(path.get(0));
            } catch (DebuggerAbsentInformationException e) {
                return null;
            }
            offset = 1;
        }

        for (int i = offset; variable != null && i < path.size(); i++) {
            variable = variable.getValue().getVariableByName(path.get(i));
        }

        if (variable == null) {
            return null;
        }

        List<Variable> variables = new ArrayList<>();
        for (JdiVariable ch : variable.getValue().getVariables()) {
            VariablePathDto chPath = newDto(VariablePathDto.class).withPath(new ArrayList<>(path));
            chPath.getPath().add(ch.getName());
            if (ch instanceof JdiField) {
                JdiField f = (JdiField)ch;
                variables.add(new FieldImpl(f.getName(),
                                            true,
                                            f.getValue().getAsString(),
                                            f.getTypeName(),
                                            f.isPrimitive(),
                                            Collections.<Variable>emptyList(),
                                            chPath,
                                            f.isFinal(),
                                            f.isStatic(),
                                            f.isTransient(),
                                            f.isVolatile()));
            } else {
                // Array element.
                variables.add(new VariableImpl(ch.getTypeName(),
                                               ch.getName(),
                                               ch.getValue().getAsString(),
                                               ch.isPrimitive(),
                                               chPath,
                                               Collections.emptyList(),
                                               true));
            }
        }
        return new SimpleValueImpl(variables, variable.getValue().getAsString());
    }

    @Override
    public void setValue(Variable variable) throws DebuggerException {
        StringBuilder expression = new StringBuilder();
        for (String s : variable.getVariablePath().getPath()) {
            if ("static".equals(s)) {
                continue;
            }
            // Here we need !s.startsWith("[") condition because
            // we shouldn't add '.' between arrayName and index of a element
            // For example we can receive ["arrayName", "[index]"]
            if (expression.length() > 0 && !s.startsWith("[")) {
                expression.append('.');
            }
            expression.append(s);
        }
        expression.append('=');
        expression.append(variable.getValue());
        evaluate(expression.toString());
    }

    @Override
    public void handleEvents(com.sun.jdi.event.EventSet eventSet) throws DebuggerException {
        boolean resume = true;
        try {
            for (com.sun.jdi.event.Event event : eventSet) {
                LOG.debug("New event: {}", event);
                if (event instanceof com.sun.jdi.event.BreakpointEvent) {
                    lock.lock();
                    try {
                        resume = processBreakPointEvent((com.sun.jdi.event.BreakpointEvent)event);
                    } finally {
                        lock.unlock();
                    }
                } else if (event instanceof com.sun.jdi.event.StepEvent) {
                    lock.lock();
                    try {
                        resume = processStepEvent((com.sun.jdi.event.StepEvent)event);
                    } finally {
                        lock.unlock();
                    }
                } else if (event instanceof com.sun.jdi.event.VMDisconnectEvent) {
                    resume = processDisconnectEvent();
                } else if (event instanceof com.sun.jdi.event.ClassPrepareEvent) {
                    resume = processClassPrepareEvent((com.sun.jdi.event.ClassPrepareEvent)event);
                }
            }
        } finally {
            if (resume) {
                eventSet.resume();
            }
        }
    }

    private boolean processBreakPointEvent(com.sun.jdi.event.BreakpointEvent event) throws DebuggerException {
        setCurrentThread(event.thread());
        boolean hitBreakpoint;
        ExpressionParser parser =
                (ExpressionParser)event.request().getProperty("org.eclipse.che.ide.java.debug.condition.expression.parser");
        if (parser != null) {
            com.sun.jdi.Value result = evaluate(parser);
            hitBreakpoint = result instanceof com.sun.jdi.BooleanValue && ((com.sun.jdi.BooleanValue)result).value();
        } else {
            // If there is no expression.
            hitBreakpoint = true;
        }

        if (hitBreakpoint) {
            com.sun.jdi.Location jdiLocation = event.location();

            Location location = debuggerUtil.getLocation(jdiLocation);
            debuggerCallback.onEvent(new SuspendEventImpl(location));
        }

        // Left target JVM in suspended state if result of evaluation of expression is boolean value and true
        // or if condition expression is not set.
        return !hitBreakpoint;
    }

    private boolean processStepEvent(com.sun.jdi.event.StepEvent event) throws DebuggerException {
        setCurrentThread(event.thread());
        com.sun.jdi.Location jdiLocation = event.location();

        Location location = debuggerUtil.getLocation(jdiLocation);
        debuggerCallback.onEvent(new SuspendEventImpl(location));
        return false;
    }

    private boolean processDisconnectEvent() {
        debuggerCallback.onEvent(new DisconnectEventImpl());
        eventsCollector.stop();
        return true;
    }

    private boolean processClassPrepareEvent(com.sun.jdi.event.ClassPrepareEvent event) throws DebuggerException {
        setCurrentThread(event.thread());
        final String className = event.referenceType().name();

        // add deferred breakpoints
        List<Breakpoint> breakpointsToAdd = deferredBreakpoints.get(className);
        if (breakpointsToAdd != null) {

            for (Breakpoint b : breakpointsToAdd) {
                addBreakpoint(b);
            }
            deferredBreakpoints.remove(className);

            // All deferred breakpoints for className have been already added,
            // so no need to listen for an appropriate ClassPrepareRequests any more.
            ClassPrepareRequest request = classPrepareRequests.remove(className);
            if (request != null) {
                getEventManager().deleteEventRequest(request);
            }
        }
        return true;
    }

    @Override
    public void stepOver(StepOverAction action) throws DebuggerException {
        doStep(StepRequest.STEP_OVER);
    }

    @Override
    public void stepInto(StepIntoAction action) throws DebuggerException {
        doStep(StepRequest.STEP_INTO);
    }

    @Override
    public void stepOut(StepOutAction action) throws DebuggerException {
        doStep(StepRequest.STEP_OUT);
    }

    private void doStep(int depth) throws DebuggerException {
        lock.lock();
        try {
            clearSteps();

            StepRequest request = getEventManager().createStepRequest(getCurrentThread(), StepRequest.STEP_LINE, depth);
            request.addCountFilter(1);
            request.enable();

            resume(newDto(ResumeActionDto.class));
        } finally {
            lock.unlock();
        }
    }

    private void clearSteps() throws DebuggerException {
        List<StepRequest> snapshot = new ArrayList<>(getEventManager().stepRequests());
        for (StepRequest stepRequest : snapshot) {
            if (stepRequest.thread().equals(getCurrentThread())) {
                getEventManager().deleteEventRequest(stepRequest);
            }
        }
    }

    @Override
    public String evaluate(String expression) throws DebuggerException {
        com.sun.jdi.Value result = evaluate(ExpressionParser.newInstance(expression));
        return result == null ? "null" : result.toString();
    }

    private com.sun.jdi.Value evaluate(ExpressionParser parser) throws DebuggerException {
        final long startTime = System.currentTimeMillis();
        try {
            return parser.evaluate(new Evaluator(vm, getCurrentThread()));
        } catch (ExpressionException e) {
            throw new DebuggerException(e.getMessage());
        } finally {
            final long endTime = System.currentTimeMillis();
            LOG.debug("==>> Evaluate time: {} ms", (endTime - startTime));
            // Evaluation of expression may update state of frame.
            resetCurrentFrame();
        }
    }

    private ThreadReference getCurrentThread() throws DebuggerException {
        if (thread == null) {
            throw new DebuggerException("Target Java VM is not suspended. ");
        }
        return thread;
    }

    private JdiStackFrame getCurrentFrame() throws DebuggerException {
        if (stackFrame != null) {
            return stackFrame;
        }
        try {
            stackFrame = new JdiStackFrameImpl(getCurrentThread().frame(0));
        } catch (IncompatibleThreadStateException e) {
            throw new DebuggerException("Thread is not suspended. ", e);
        }
        return stackFrame;
    }

    private void setCurrentThread(ThreadReference t) {
        stackFrame = null;
        thread = t;
    }

    private void resetCurrentFrame() {
        stackFrame = null;
    }

    private void resetCurrentThread() {
        this.stackFrame = null;
        this.thread = null;
    }

    private EventRequestManager getEventManager() throws DebuggerException {
        try {
            return vm.eventRequestManager();
        } catch (VMCannotBeModifiedException e) {
            throw new DebuggerException(e.getMessage(), e);
        }
    }
}
